package org.nhindirect.stagent;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import org.nhindirect.stagent.EntityPart.EntityPartType;
import org.nhindirect.stagent.ProtocolException.ProtocolError;

public class EntityParser 
{
    /// <summary>
    /// A set of prototype lightweight parsing routines. They utilize .NET's "lazy evaluation/LINQ" technique to generate
    /// a set of enumerators of the source - that can then be further composed into higher level constructs
    /// and integrated with LINQ, if required. 
    /// 
    /// A more FORMAL parser that includes a Lexer + explicitly follows the RFC2822 grammar and adheres to
    /// more classic Parser Patterns - will be written separately.
    /// 
    /// One key requirement for parsing is that underlying raw entity text is always retained VERBATIM. 
    /// For cryptography, it is important that post-parsing, we be able to recover the original entity AS IS.
    /// The parser therefore works exclusively with StringSegments - a lightweight struct NOT allocated from the Heap.
    /// StringSegment maintains offsets into the original entity text. 
    /// 
    /// This design has the added benefit of minimizing string allocations (substrings etc), since no strings are
    /// actually allocated. 
    /// 
    /// </summary>

        public static MimeEntity read(String entityText)
        {
            return EntityParser.read(new StringSegment(entityText));
        }
        
        public static MimeEntity read(StringSegment entityText)
        {
        	MimeEntity entity = new MimeEntity();
            for (EntityPart part : EntityParser.readEntityParts(entityText))
            {
                switch(part.getType())
                {
                    default:
                        break;
                        
                    case Header:
                        entity.getHeaders().add((Header) part);
                        break;
                    
                    case Body:
                        if (entity.hasBody())
                        {
                            throw new ProtocolException(ProtocolError.InvalidBody);
                        }
                        entity.setBody((Body) part);
                        break;
                }
            }
            
            return entity;
        }
        
        public static Collection<StringSegment> ReadHeaderPart(StringSegment source, char separator)
        {
        	Collection<StringSegment> retVal = new ArrayList<StringSegment>();
        	
            CharReader reader = new CharReader(source);
            int startAt = reader.getPosition();
            while (reader.readTo(separator))
            {
                if (reader.isPrev(Protocol.Escape))
                {
                    continue;
                }
                
                retVal.add(new StringSegment(source.getSource(), startAt, reader.getPosition() - 1));
                
                startAt = reader.getPosition() + 1;
            }            
            
            StringSegment last = new StringSegment(source.getSource(), startAt, reader.getPosition() - 1);
            if (!last.isEmpty())
            {
            	retVal.add(last);
            }
            
            return retVal;
        }
        
        public static Collection<EntityPart> ReadBodyParts(String entity, String boundary)
        {
            return EntityParser.readBodyParts(new StringSegment(entity), boundary);
        }

        public static Collection<EntityPart> readBodyParts(StringSegment entity, String boundary)
        {
            return EntityParser.ReadBodyParts(EntityParser.readLines(entity), boundary);
        }
        
        public static Collection<EntityPart> ReadBodyParts(Collection<StringSegment> bodyLines, String boundary)
        {
        	Collection<EntityPart> retVal = new ArrayList<EntityPart>();
        	
            if (bodyLines == null)
            {
                throw new IllegalArgumentException();
            }
            
            Iterator<StringSegment> lineIter = bodyLines.iterator();
            
            StringSegment part = StringSegment.createNull();
            StringSegment prevLine = StringSegment.createNull();
            EntityPartType expectedPart = EntityPartType.Prologue;
            
            //
            // As per the Multipart Spec:
            //  The boundary delimiter is actually CRLF--boundary
            // So we must maintain a stack (prevLine)..
            //            
            while (expectedPart != EntityPartType.Epilogue && lineIter.hasNext())
            {
                StringSegment line = lineIter.next();
                if (isBoundary(line, boundary))
                {
                    if (!part.isNull())
                    {
                        part.union(prevLine);
                    }
                    //
                    // When we hit a boundary, we yield the part we've collected so far
                    //
                    EntityPartType partType = expectedPart;
                    switch(expectedPart)
                    {
                        default:
                            throw new ProtocolException(ProtocolError.InvalidBodySubpart);
                        
                        case Prologue:
                            expectedPart = EntityPartType.SubPart;
                            break;
                        
                        case SubPart:
                            if (isBoundaryEnd(line))
                            {   
                                expectedPart = EntityPartType.Epilogue;
                            }
                            break;
                    }
                    
                    if (!part.isNull())
                    {      
                    	retVal.add(new EntityPart(partType, part));
                    }
                    
                    prevLine = StringSegment.createNull();
                    part = StringSegment.createNull();
                }
                else
                {
                    if (!prevLine.isNull())
                    {
                        part.union(prevLine);
                    }
                    
                    prevLine = line;
                }
            }
            
            if (expectedPart != EntityPartType.Epilogue)
            {
                throw new ProtocolException(ProtocolError.InvalidBodySubpart);
            }
            
            // Epilogue
            part = StringSegment.createNull(); 
            while (lineIter.hasNext())
            {
                part.union(lineIter.next());
            }
            if (!part.isEmpty())
            {
            	retVal.add(new EntityPart(EntityPartType.Epilogue, part));
            }
            return retVal;
        }
        
        static boolean isBoundary(StringSegment line, String boundary)
        {
            int length = line.getLength();
            return (length > 1
                    && length >= (boundary.length() + 2)
                    && line.charAt(0) == Protocol.BoundaryChar
                    && line.charAt(1) == Protocol.BoundaryChar 
                    && (String.CASE_INSENSITIVE_ORDER.compare(boundary, line.toString().substring(2, (boundary.length() + 2))) == 0));
 
        }
        
        static boolean isBoundaryEnd(StringSegment line)
        {
            int length = line.getLength();
            return (length > 4
                    && line.charAt(length - 1) == Protocol.BoundaryChar
                    && line.charAt(length - 2) == Protocol.BoundaryChar);
        }
        
        public static Collection<EntityPart> readEntityParts(String entity)
        {
            return EntityParser.readEntityParts(new StringSegment(entity));
        }

        public static Collection<EntityPart> readEntityParts(StringSegment entity)
        {
            return EntityParser.readEntityParts(EntityParser.readLines(entity));
        }
        
        public static Collection<EntityPart> readEntityParts(Collection<StringSegment> lines)
        {
        	Collection<EntityPart> retVal = new ArrayList<EntityPart>();
        	
            if (lines == null)
            {
                throw new IllegalArgumentException();
            }
            
            EntityPartType expectedPartType = EntityPartType.Header;
            Header header = null;
            Body body = null;
            
            for(StringSegment line : lines)
            {
                switch(expectedPartType)
                {
                    default:
                        throw new ProtocolException(ProtocolError.Unexpected);
                        
                    case Header:                        
                        if (line.isEmpty())
                        {
                            if (header != null)
                            {
                                //yield return header;
                            	retVal.add(header);
                                header = null;
                            }
                            
                            retVal.add(new EntityPart(EntityPartType.HeaderBoundary, line));                            
                            //                            
                            // Done with the header section. Onto the Body
                            //
                            expectedPartType = EntityPartType.Body;
                            break;
                        }

                        if (Protocol.isWhitespace(line.charAt(0)))
                        {
                            // Line folding. This line belongs to the current header
                            if (header == null)
                            {
                                throw new ProtocolException(ProtocolError.InvalidHeader);
                            }
                            header.appendSourceText(line);
                            break;
                        }
                        
                        if (header != null)
                        {
                        	retVal.add(header);
                        }
                        header = new Header(line);     
                        break;
                    
                    case Body:
                        if (body == null)
                        {
                            body = new Body(line);
                        }
                        else
                        {
                            body.appendSourceText(line);
                        }
                        break;                        
                }
            }
            
            if (header != null)
            {
            	retVal.add(header);
            }
            if (body != null)
            {
            	retVal.add(body);
            }
            
            return retVal;
        }
        
        public static Collection<StringSegment> readLines(String entity)
        {
            return EntityParser.readLines(new StringSegment(entity));
        }
        
        public static Collection<StringSegment> readLines(StringSegment entity)
        {
        	Collection<StringSegment> retVal = new ArrayList<StringSegment>();
        	
            CharReader reader = new CharReader(entity);
            int startIndex = reader.getPosition() + 1;
            int endIndex = startIndex - 1;            
            char ch;
            
            while ((ch = reader.read()) != Character.MIN_VALUE)
            {
                switch (ch)
                {
                    case Protocol.CR:
                        //
                        // RFC 2822 mandates that CRLF be together always
                        //
                        if (reader.read() != Protocol.LF)
                        {
                            throw new ProtocolException(ProtocolError.InvalidCRLF);
                        }
                        retVal.add(new StringSegment(reader.getSource(), startIndex, endIndex));
                        
                        startIndex = reader.getPosition() + 1;
                        endIndex = reader.getPosition();
                        break;
                        
                    case Protocol.LF:
                        //
                        // No standalone LF allowed
                        //
                        throw new ProtocolException(ProtocolError.InvalidCRLF);
                        
                    default:
                        endIndex = reader.getPosition();
                        break;
                }
            }
        
            if (endIndex >= 0)
            {
            	retVal.add(new StringSegment(reader.getSource(), startIndex, endIndex));    
            }
            
            return retVal;
        }
        
        public static StringSegment skipWhitespace(StringSegment text)
        {
            CharReader reader = new CharReader(text); // Struct. Typically created on stack, so pretty efficient
            char ch;
            while ((ch = reader.read()) != Character.MIN_VALUE && Protocol.isWhitespace(ch));
            
            text.setStartIndex(reader.getPosition());
            return text;  // StringSegment is a struct, so this will return a copy
        }
        
        public static int findChar(StringSegment text, char ch)
        {
            return findChar(text, ch, false);
        }
        
        public static int findChar(StringSegment text, char ch, boolean specialChar)
        {
            CharReader reader = new CharReader(text);
            while (reader.readTo(ch))
            {
                if (!specialChar || !reader.isPrev(Protocol.Escape))
                {
                    return reader.getPosition();
                }
            }
            
            return -1;
        }
}
